package saves;

import com.esotericsoftware.kryo.Kryo;
import game.TextManager;
import images.ShipLoader;
import java.io.Serializable;
import java.util.ArrayList;
import menu.ItemInterface.TransItem;
import saves.Inventory.ItemEx;
import saves.items.Consumivel;
import saves.items.Efeito;
import saves.items.Equipamento;
import saves.items.Equipamento.LocalEquip;
import saves.items.Item;
import saves.items.Municao;
import saves.objects.Nave.ActionResults;

public class EntidadeNave implements Serializable {

    //constants
    private static final long serialVersionUID = 7392135659187021571L;
    private static final int[] DEFAULT_UPGRADES, DEFAULT_ORBS;
    public static final int DEFAULT_PERCEPTION_DISTANCE = 400, DEFAULT_COMUNICATION_DISTANCE = 200, DEFAULT_SELECT_DISTANCE = 150;
    public static final int PONTOS_EXTRAS_NIVEL = 2;
    static { //starting upgrades
        DEFAULT_UPGRADES = new int [TextManager.t().UPGRADES_NAMES.length];
        for (int i = 0; i < DEFAULT_UPGRADES.length; i++)
            DEFAULT_UPGRADES[i] = 1;
        
        DEFAULT_ORBS = new int [TextManager.t().ATTRIBUTES.length];
    }
    
    //personal information
    private int identificacao;
    private String nome;
    private CorpoOrbitavel localNascimento; //pointer
    private Faccao faccao; //pointer
    private Conhecimentos c;

    //items
    private Inventory inv;
    private Equipamento[] equips;
    private float equipsMass;
    
    //stats
    private int[] status, upgrades, orbs;
    private int pontosDisponiveis;
    private ArrayList<Efeito> efeitos;
    private int nivel;
    private float vidaAtual, energiaAtual, xpAtual;

    //quests
    private ArrayList<Missao> missoesRecebidas, missoesDadas;
    private int missaoAtual;
    private float tempoNaMissao;

    @SuppressWarnings("unused")
    private EntidadeNave() { }

    public EntidadeNave(String nome, CorpoOrbitavel localNascimento, Conhecimentos c, int[] status, Faccao faccao) {
        this(nome, localNascimento, c, new ArrayList<ItemEx>(), DEFAULT_UPGRADES, status, DEFAULT_ORBS, new ArrayList<Missao>(), new ArrayList<Missao>(), -1, -1, faccao);
    }

    public EntidadeNave(String nome, CorpoOrbitavel localNascimento, Conhecimentos c, ArrayList<ItemEx> inventario, int[] upgrades, int[] status, int[] orbs, ArrayList<Missao> missoesRecebidas, ArrayList<Missao> missoesDadas, int missaoAtual, float tempoNaMissao, Faccao faccao) {
        //personal information
        this.nome = nome;
        this.localNascimento = localNascimento;
        this.c = c;
        this.faccao = faccao;

        //items
        this.equips = new Equipamento[5];
        this.inv = new Inventory(inventario, 100, 100000); //todo change starting money (?) and vol limit (?)
        this.upgrades = upgrades;
        
        //atributes
        this.status = status;
        this.orbs = orbs;
        this.vidaAtual = status[0];
        this.energiaAtual = status[1];
        this.efeitos = new ArrayList<>();
        attributesChanged();
        
        //leveling
        this.xpAtual = 1;
        this.nivel = 1;
        this.pontosDisponiveis = 0;
        
        //quests
        this.missoesRecebidas = missoesRecebidas;
        this.missoesDadas = missoesDadas;
        this.missaoAtual = missaoAtual;
        this.tempoNaMissao = tempoNaMissao;
    }
    
    public boolean isDisabled() {
        return this.vidaAtual < 0;
    }
    
    private void attributesChanged() {
        calculateEquipsMass();
        inv.setVolumeLimit(getAttributeModifier(Status.CARGO.p(), 1));
    }
    
    private void calculateEquipsMass() {
        equipsMass = 0;
        for (int i = 0; i < equips.length; i++)
            if (equips[i] != null)
                equipsMass += equips[i].getMassa();
    }
    
    public float getMass() {
        return inv.getMass() + equipsMass;
    }
    
    public float getInventoryVolume() {
        return inv.getVolume();
    }
    
    public void addEfeito(Efeito e) {
        efeitos.add(new Kryo().copy(e));
        attributesChanged();
    }

    public String getNome() {
        return nome;
    }

    public Equipamento getEquip(int pos) {
        return this.equips[pos];
    }
    
    public void setEquips(Equipamento[] eqs) {
        System.arraycopy(eqs, 0, equips, 0, equips.length);
        attributesChanged();
    }

    public CorpoOrbitavel getLocalNascimento() {
        return localNascimento;
    }

    public int getIdentificacao() {
        return identificacao;
    }
    
    public Inventory getInventory() {
        return this.inv;
    }

    public int[] getUpgrades() {
        return upgrades;
    }

    public Faccao getFaccao() {
        return faccao;
    }
    
    private boolean isEquipped(LocalEquip e) {
        for (int i = 0; i <= e.p(); i++)
            if (equips[i] != null && equips[i].is(e))
                return true;
        return false;
    }
    
    public static float getModifier(float x) {
        final double c = 1.8/Math.PI;
        return (float) (c*Math.atan(x/100));
    }

    public float getAttributeModifier(int c1, int c2) {
        float p = getBonus(c1, c2);
        if (c1 <= Status.ENERGY.p() && c2 == 0)
            return 10*p; //life or energy
        if (c2 == 0)
            return -1; //other base attributes don't have modifiers
        
        if (c1 == Status.SPEED.p()) {
            if ((c2 == 1 && !isEquipped(LocalEquip.PROP_TRASEIRO)) || (c2 == 1 && !isEquipped(LocalEquip.PROP_LATERAL)))
                return 0; //no proper thruster is equipped
            return c2 == 1 ? 4*p : p/4; //? linear speed : angular speed
        }
        if (c1 == Status.CARGO.p() && c2 == 1) { //volume limit
            //(def_cargo + def_cargo_per_stat_point * stat_points) * 10 if singularity_cargo
            return (8 + .2f * p) * (upgrades[3] == 1 ? 10 : 1);
        }
        
        //this could be life, energy, off, def
        return getModifier(p); //bonus/reductor on each attack/resistence type
    }

    public float getAttribute(int c1, int c2) {
        float valor;
        if (c2 == 0)
            valor = getStatus(c1);
        else
            valor = getAttribute(c1, 0);
        //Aplicar efeitos de equips
        for (int i = 0; i < equips.length; i++)
            if (equips[i] != null)
                valor = equips[i].modificar(c1, c2, valor);
        //Aplicar efeitos do vetor de efeitos (consumíveis, buffs, debuffs, etc)
        for (int i = 0; i < efeitos.size(); i++)
            if (efeitos.get(i).apply(c1, c2))
                valor = efeitos.get(i).modificar(valor);

        return valor;
    }
    
    public ActionResults fire(int type, float energy) {
        if (this.isDisabled())
            return ActionResults.SHIP_DISABLED;
        if (type == -1)
            return consumir(energy) ? ActionResults.OK : ActionResults.OUT_OF_EENRGY;
        Municao ammo = new Municao(type);
        if (inv.possue(ammo)) {
            if (consumir(energy))
                return inv.removeItem(ammo) ? ActionResults.OK : ActionResults.OUT_OF_PROJECTILES;
            return ActionResults.OUT_OF_EENRGY;
        }
        return ActionResults.OUT_OF_PROJECTILES;
    }
    
    public float getBonus(int c1, int c2) {
        float bonus = getAttribute(c1, c2);
        if (c2 == 0)
            return bonus;
        return getAttribute(c1, 0) + bonus;
    }
    
    public float getStatus(int codStatus) {
        return status[codStatus] + orbs[codStatus];
    }

    public int getOrbs(int codAtributo) {
        return orbs[codAtributo];
    }
    
    public void update(int delta) {
        //default solar panel energy increase
        if (isDisabled())
            return;
        consumir(-.1f); //todo calibrate with current ilumination
        
        boolean nok = false;
        for (int i = 0; i < efeitos.size(); i++)
            if (efeitos.get(i).tick(delta)) {
                efeitos.remove(--i);
                nok = true;
            }
        
        if (nok)
            attributesChanged();
    }
    
    public boolean wouldAccept(ArrayList<TransItem> it, double tm) {
        double realValue = 0;
        for (int i = 0; i < it.size(); i++)
            realValue += (it.get(i).isUp() ? -1 : 1) * it.get(i).getItem().getIndicePreco()*100;
        //todo apply any margins for bargain, friendship, hate, important/necessary items, imediate request, etc
        
        //if real value is positive, this npc is paying; otherwise, it's receiving
        return realValue >= tm; //this should work on every possible case
    }
    
    public float getWingHeight() {
        if (equips[1] == null)
            return ShipLoader.modelos.get(equips[0].getCodModelo()).getWingHeight();
        return ShipLoader.modelos.get(equips[1].getCodModelo()).getWingHeight();
    }

    public static final class Conhecimentos {
        private float indiceAleatorio, conhecimentosLocais, conhecimentosItens, conhecimentosFaccoes;

        public Conhecimentos(float indiceAleatorio, float conhecimentosLocais, float conhecimentosItens, float conhecimentosFaccoes) {
            if (!setIndiceAleatorio(indiceAleatorio))
                this.indiceAleatorio = 0.5f;
            if (!setConhecimentosLocais(conhecimentosLocais))
                this.conhecimentosLocais = 0.5f;
            if (!setConhecimentosItens(conhecimentosItens))
                this.conhecimentosItens = 0.5f;
            if (!setConhecimentosFaccoes(conhecimentosFaccoes))
                this.conhecimentosFaccoes = 0.5f;
        }

        public float getIndiceAleatorio() {
            return indiceAleatorio;
        }

        public boolean setIndiceAleatorio(float f) {
            if (f < 0 || f > 1)
                return false;
            this.indiceAleatorio = f;
            return true;
        }

        public float getConhecimentosLocais() {
            return conhecimentosLocais;
        }

        public boolean setConhecimentosLocais(float f) {
            if (f < 0 || f > 1)
                return false;
            this.conhecimentosLocais = f;
            return true;
        }

        public float getConhecimentosItens() {
            return conhecimentosItens;
        }

        public boolean setConhecimentosItens(float f) {
            if (f < 0 || f > 1)
                return false;
            this.conhecimentosItens = f;
            return true;
        }

        public float getConhecimentosFaccoes() {
            return conhecimentosFaccoes;
        }

        public boolean setConhecimentosFaccoes(float f) {
            if (f < 0 || f > 1)
                return false;
            this.conhecimentosFaccoes = f;
            return true;
        }
    }

    public int getNivel() {
        return this.nivel;
    }
    
    public int getPontosDisponiveis() {
        return this.pontosDisponiveis;
    }
    
    public boolean upgradeStatus(int codigo) {
        if (pontosDisponiveis > 0) {
            status[codigo]++;
            pontosDisponiveis--;
            attributesChanged();
            return true;
        }
        return false;
    }

    public boolean ganharExp(float xp) {
        this.xpAtual += xp;
        
        //ver se upou
        if (this.xpAtual >= getMaxXP()) {
            float diff = this.xpAtual - getMaxXP();
            this.xpAtual = 0;
            this.nivel++;
            this.pontosDisponiveis += PONTOS_EXTRAS_NIVEL;
            ganharExp(diff);
            return true;
        }
        return false;
    }

    public float getVidaAtual() {
        float maxh = getAttributeModifier(0, 0);
        if (vidaAtual > maxh)
            vidaAtual = maxh;
        return vidaAtual;
    }

    public float getEnergiaAtual() {
        float maxe = getAttributeModifier(1, 0);
        if (energiaAtual > maxe)
            energiaAtual = maxe;
        return energiaAtual;
    }

    public float getXpAtual() {
        return xpAtual;
    }
    
    public int getXpWorth() {
        return 30;
    }
    
    public void replenish() {
        this.vidaAtual = getAttributeModifier(0, 0);
        this.energiaAtual = getAttributeModifier(1, 0);
    }
    
    /**
     * Use an item (equip, unequip, consume, etc.)
     * @param it The item to be used.
     * @return The result of the operation. Can be:
     *  -4: Ship is disabled, can't use items
     *  -3: The operation will extrapolate volume limits
     *  -2: Don't have the preequisits
     *  -1: Don't have the item
     *   0: Can't use this kind of item
     *   1: OK
     */
    public int usar(Item it) {
        if (isDisabled())
            return -4;
        int re = usar2(it);
        if (re == 1 || re == -3) //-3 may still unequip some items in the process before realizing the error
            attributesChanged();
        return re;
    }

    private int usar2(Item it) {
        if (it instanceof Consumivel) {
            if (((Consumivel) it).podeUsar(this)) {
                if (inv.removeItem(it))
                    ((Consumivel) it).usar(this);
                else
                    return -1; //Não possui
            } else
                return -2; //Não preenche pré-requisitos
        } else if (it instanceof Equipamento) {
            Equipamento eq = (Equipamento) it;
            
            //Ver se está equipado!
            for (int i = 0; i < equips.length; i++)
                if (equips[i] != null && equips[i].equals(eq)) {
                    //Desequipar
                    if (equips[i].is(LocalEquip.CARCACA))
                        return -2; //você não pode desequipar sua carcaça!
                    
                    //se for uma asa e se tiver prop. lat. ou trans. en. não pode desequipar!
                    if (equips[i].is(LocalEquip.ASA) && (equips[LocalEquip.PROP_LATERAL.p()] != null || equips[LocalEquip.TRANSFORMADOR.p()] != null))
                        return -2; //você não pode
                    
                    if (inv.addItem(eq)) {
                        equips[i] = null;
                        return 1;
                    } else
                        return -3;
                }
            
            if (eq.podeEquipar(this)) {
                if (inv.removeItem(it)) {
                    LocalEquip[] loc = eq.getCodLocais();
                    if (equips[loc[0].p()] != null)
                        if (!inv.addItem(equips[loc[0].p()]))
                            return -3;
                    for (int i = 1; i < loc.length; i++) {
                        if (equips[loc[i].p()] != null) {
                            if (!inv.addItem(equips[loc[i].p()]))
                                return -3;
                            equips[loc[i].p()] = null;
                        }
                    }
                    //if everythig went well
                    equips[loc[0].p()] = eq;
                } else
                    return -1; //Não possui
            } else
                return -2; //Não preenche pré-requisitos
        } else
            return 0; //Não dá para usar
        
        return 1; //OK
    }

    public boolean consumir(float energia) {
        if (this.energiaAtual < energia)
            return false;
        this.energiaAtual -= energia;
        float maxe = this.getAttributeModifier(Status.ENERGY.p(), 0);
        if (this.energiaAtual > maxe)
            this.energiaAtual = maxe;
        return true;
    }

    public float getFracaoVida() {
        return this.vidaAtual / this.getAttributeModifier(Status.LIFE.p(), 0);
    }

    public float getFracaoEnergia() {
        return this.energiaAtual / this.getAttributeModifier(Status.ENERGY.p(), 0);
    }

    public float getFracaoXP() {
        return this.xpAtual / getMaxXP();
    }
    
    public float getMaxXP() {
        return (float) Math.pow(this.nivel, 2)*2 + 30;
    }
    
    public void causarDano(float dano, DamageType type) {
        //todo apply defenses, etc etc.
        this.vidaAtual -= dano;
        if (this.vidaAtual < 0) //disabled
            this.efeitos.clear(); //remove all effects
    }
    
    public boolean destroy() {
        return this.getFracaoVida() <= -.1f;
    }
    
    public String getEffectsOn(int c1, int c2) {
        return Equipamento.getEffectsOn(efeitos.toArray(new Efeito[efeitos.size()]), c1, c2);
    }
    
    public enum DamageType {
        PROJECTILE, EXPLOSION, HEAT, ELECTRICITY;
    }
    
    public enum Status {
        LIFE, ENERGY, OFF, DEF, SPEED, CARGO;
        
        public int p() {
            switch(this) {
                case LIFE: return 0;
                case ENERGY: return 1;
                case OFF: return 2;
                case DEF: return 3;
                case SPEED: return 4;
                default: return 5; //CARGO
            }
        }
    }
}
